/*
 * Copyright (c) 2020 Fraunhofer FOKUS and others. All rights reserved.
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contact: mosaic@fokus.fraunhofer.de
 */
package org.eclipse.mosaic.lib.geo;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class CartesianSector implements Sector<CartesianPoint>, CartesianArea {

    private static final long serialVersionUID = 1L;

    private final CartesianPoint center;
    private final double radius;

    /**
     * north = 0 south = 180
     */
    private final double begin;
    private final double end;

    public CartesianSector(CartesianPoint center, double radius, double begin, double end) {
        this.center = center;
        this.radius = radius;
        this.begin = begin;
        this.end = end;
    }

    @Override
    public Bounds<CartesianPoint> getBounds() {
        double angleDeg = end - begin;

        CartesianPoint point1;
        CartesianPoint point2;

        if (angleDeg > 0 && angleDeg <= 90) {
            point1 = CartesianPoint.xy(center.getX() + radius * Math.sin(Math.toRadians(begin)),
                            center.getY() + getRadius() * Math.cos(Math.toRadians(begin)));
            point2 = CartesianPoint.xy(center.getX() + radius * Math.sin(Math.toRadians(begin + 90)),
                    center.getY() + getRadius() * Math.cos(Math.toRadians(begin + 90)));
        } else if (angleDeg > 90 && angleDeg <= 180) {
            double delta = (180 - angleDeg) / 2;
            point1 = CartesianPoint.xy(center.getX() + radius * Math.sin(Math.toRadians(begin - delta)),
                    center.getY() + getRadius() * Math.cos(Math.toRadians(begin - delta)));
            point2 = CartesianPoint.xy(center.getX() + radius * Math.sin(Math.toRadians(begin - delta + 90)),
                    center.getY() + getRadius() * Math.cos(Math.toRadians(begin - delta + 90)));
        } else if (angleDeg > 180 && angleDeg <= 270) {
            double delta = (360 - angleDeg) / 2;
            double gama = begin + (90 - Math.toDegrees(Math.atan(Math.cos(Math.toRadians(delta)))) - delta);

            point1 = CartesianPoint.xy(center.getX() + radius * Math.sin(Math.toRadians(gama)),
                    center.getY() + getRadius() * Math.cos(Math.toRadians(gama)));
            point2 = CartesianPoint.xy(center.getX() + radius * Math.sin(Math.toRadians(gama + 180)),
                    center.getY() + getRadius() * Math.cos(Math.toRadians(gama + 180)));
        } else {
            throw new IllegalArgumentException(String.format("begin: %.5f, end: %.5f, cannot find Bounds", begin, end));
        }

        return new CartesianRectangle(point1, point2);
    }

    @Override
    public GeoArea toGeo() {
        return new GeoSector(center.toGeo(), radius, begin, end);
    }

    @Override
    public CartesianPoint getCenter() {
        return center;
    }

    @Override
    public double getRadius() {
        return radius;
    }

    @Override
    public double getBegin() {
        return begin;
    }

    @Override
    public double getEnd() {
        return end;
    }

    @Override
    public double getRadians() {
        return Math.toRadians(end - begin);
    }

    @Override
    public Polygon<CartesianPoint> toPolygon() {
        return toPolygon(1);
    }

    public CartesianPolygon toPolygon(double degreeResolution) {
        final List<CartesianPoint> vertices = new ArrayList<>();
        vertices.add(center);

        int steps = (int) ((end - begin) / degreeResolution);
        for (int i = 0; i <= steps; i++) {
            double radians = Math.toRadians(begin + i * degreeResolution);
            vertices.add(CartesianPoint.xy(
                    getCenter().getX() + getRadius() * Math.sin(radians),
                    getCenter().getY() + getRadius() * Math.cos(radians)
            ));
        }
        return new CartesianPolygon(vertices);
    }

    @Override
    public boolean contains(CartesianPoint point) {
        double degree = GeoUtils.azimuth(center, point);

        return getCenter().distanceTo(point) <= getRadius() && degree >= begin && degree <= end;
    }

    @Override
    public String toString() {
        return String.format(Locale.ENGLISH, "Cartesian{center=%s,radius=%.2f,begin=%.2f,end=%.2f}", this.center,
                this.radius, this.begin, this.end);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        CartesianSector other = (CartesianSector) o;
        return Double.compare(radius, other.getRadius()) == 0
                && this.getCenter().equals(other.getCenter())
                && this.getBegin() == other.getBegin()
                && this.getEnd() == other.getEnd();
    }

    @Override
    public int hashCode() {
        long longHash = 6618333L;
        longHash = 31 * longHash + Double.doubleToLongBits(radius);
        longHash = 31 * longHash + getCenter().hashCode();
        longHash = 31 * longHash + Double.doubleToLongBits(begin);
        longHash = 31 * longHash + Double.doubleToLongBits(end);
        return (int) (longHash ^ (longHash >>> 32));
    }
}
